package main

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/cookiejar"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	_ "strconv"
	"strings"
	"time"

	"github.com/julienschmidt/httprouter"
	"github.com/mikepb/go-serial"
	tserial "github.com/tarm/serial"
)

var (
	crtFile  = "https://www.codepku.com/misc/local.codepku/local.crt"
	keyFile  = "https://www.codepku.com/misc/local.codepku/local.key"
	infoFile = "https://www.codepku.com/misc/local.codepku/ardu.txt"
	PORT     = "26337" // 端口（手机 codepku前几位）
)

type PortInfo struct {
	Name string
	Desc string
	Info *serial.Info
}

// used to return of http request (as json)
type RespStatus struct {
	Code   int
	Msg    string
	Detail string
}

func getSerialPortsInfo() ([]PortInfo, error) {
	ports, err := serial.ListPorts()
	if err != nil {
		return nil, err
	}

	portList := make([]PortInfo, 0, 5)
	for _, p := range ports {
		pt := PortInfo{p.Name(), p.Name() + "&" + p.Description() + "&" + p.USBManufacturer() + "&" + p.USBProduct(), p}

		portList = append(portList, pt)
	}

	return portList, nil
}

func findArduinoPort() (*PortInfo, bool) {
	portList, _ := getSerialPortsInfo()

	// log.Println(portList)
	for _, p := range portList {
		//很多第三方Arduino板子都是ch340芯片，认为ch340也是arduino板子
		if strings.Contains(strings.ToLower(p.Desc), "arduino") ||
			strings.Contains(strings.ToLower(p.Desc), "ch340") ||
			strings.Contains(strings.ToLower(p.Desc), "wchusbserial") { // ch340芯片在Mac下显示wchusbserial1420
			return &p, true
		}
	}

	return nil, false
}

func saveCodeToFile(code string) string {
	tmpdir, err := ioutil.TempDir("", "arduino")
	if err != nil {
		log.Fatal("Failed to create temp folder: ", err)
	}
	// defer os.RemoveAll(tmpdir)这里不能删除，后边还要编译上传代码
	filename := tmpdir + string(os.PathSeparator) + "ardu.cpp"
	fout, err := os.Create(filename)
	log.Println("Create folder and file: " + fout.Name())
	defer fout.Close()
	if err != nil {
		log.Fatal("Failed to create file: ", err)
	}

	fout.WriteString(code)
	return filename
}

func runCommand(cmdStr string) bool {
	fmt.Println("In Run Command func, start runing: ", cmdStr)
	cmdPart := strings.Split(cmdStr, " ")
	//fmt.Println(strings.Join(cmdPart, "|"))
	cmd := exec.Command(cmdPart[0], cmdPart[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		log.Println("代码编译上传出错：请检查代码或者联系老师")
		log.Println(err) // don't exit when there is error.
		return false
	}

	return true
}

func compileCode(filename string) bool {

	ARDUINO_DIR := "./Arduino/"
	CC := ARDUINO_DIR + "hardware/tools/avr/bin/avr-gcc"
	CPP := ARDUINO_DIR + "hardware/tools/avr/bin/avr-g++"
	// AR := ARDUINO_DIR + "hardware/tools/avr/bin/avr-ar"
	OBJ_COPY := ARDUINO_DIR + "hardware/tools/avr/bin/avr-objcopy"
	BUILD_DIR := "./building/"
	// to save uploaded code, other wise (current dir) permission denied
	TEMP_DIR := filepath.Dir(filename) + string(os.PathSeparator)
	// 先关闭串口读取
	serialOpChan <- "close"

	defer func() {
		// 上传完成，打开串口读取
		go checkSerialData(serialOpChan)
		// 上传完成，删除临时文件夹
		os.RemoveAll(TEMP_DIR)
	}()
	log.Println(filename)
	log.Println(TEMP_DIR)
	F_CPU := "16000000"
	MCU := "atmega328p"
	GENERAL_FLAGS := fmt.Sprintf("-c -g -Os -Wall -ffunction-sections "+
		"-fdata-sections -mmcu=%s -DF_CPU=%sL -MMD -DUSB_VID=null -DUSB_PID=null "+
		"-DARDUINO=10605 -DARDUINO_AVR_UNO -DARDUINO_ARCH_AVR", MCU, F_CPU)

	CPP_FLAGS := GENERAL_FLAGS + " -fno-exceptions"
	//CC_FLAGS := GENERAL_FLAGS

	INCLUDE_FILES := strings.Replace("-I$(ARDUINO_DIR)hardware/arduino/avr/cores/arduino "+
		"-I$(ARDUINO_DIR)hardware/arduino/avr/variants/standard "+
		"-I$(ARDUINO_DIR)hardware/arduino/avr/libraries/Wire "+
		"-I$(ARDUINO_DIR)libraries/Servo/src "+
		"-I$(ARDUINO_DIR)libraries/BWBX_Robot "+
		"-I$(ARDUINO_DIR)libraries/IRremote "+
		"-I$(ARDUINO_DIR)libraries/MeBuzzer "+
		"-I$(ARDUINO_DIR)libraries/LiquidCrystal "+
		"-I$(ARDUINO_DIR)hardware/arduino/avr/libraries/SoftwareSerial",
		"$(ARDUINO_DIR)", ARDUINO_DIR, -1)

	// LIBRARY_DIR := ARDUINO_DIR + "hardware/arduino/avr/cores/arduino/"

	// filename := "ardu.cpp" // pass filename as argument

	// start to compile
	// todo, 临时文件可能会有空格，需要加引号
	commands := []string{
		// build .cpp main file
		CPP + " " + CPP_FLAGS + " " + INCLUDE_FILES + " -o " + filename + ".o " + filename,
		// build and connect lib .o files (basecore.a)
		fmt.Sprintf("%s -w -Os -Wl,--gc-sections -mmcu=%s -o %s.elf %s.o "+
			"%sServo/avr/Servo.cpp.o %sServo/sam/Servo.cpp.o %sWire/Wire.cpp.o "+
			"%sWire/utility/twi.c.o %sIRremote/IRremote.cpp.o %sBWBX_Robot/BWBX_Robot.cpp.o "+
			"%sMeBuzzer/MeBuzzer.cpp.o "+
			"%sLiquidCrystal/LiquidCrystal.cpp.o "+
			"basecore.a -L%s -lm",
			CC, MCU, filename,
			filename, BUILD_DIR, BUILD_DIR, BUILD_DIR, BUILD_DIR, BUILD_DIR, BUILD_DIR, BUILD_DIR, BUILD_DIR),
		//
		fmt.Sprintf("%s -O ihex -j .eeprom --set-section-flags=.eeprom=alloc,load "+
			"--no-change-warnings --change-section-lma .eeprom=0 %s.elf "+
			"%s.eep", OBJ_COPY, filename, filename),
		fmt.Sprintf("%s -O ihex -R .eeprom %s.elf %s.hex", OBJ_COPY, filename, filename),
		// upload to arduino
		fmt.Sprintf("%shardware/tools/avr/bin/avrdude -C%s/hardware/tools/avr/etc/avrdude.conf "+
			"-v -patmega328p -carduino -P%s -b115200 -D -Uflash:w:%s.hex:i",
			ARDUINO_DIR, ARDUINO_DIR, arduinoPort.Name, filename),
	}

	// 切换到临时文件夹
	for _, cmd := range commands {
		//fmt.Println("Running: " + cmd)
		if !runCommand(cmd) {
			fmt.Println("代码编译上传失败")
			return false
		}
	}

	return true
	//上传代码完成之后，需要继续监听串口

	//$(CPP) $(CPP_FLAGS) $(INCLUDE_FILES) $(MAIN_SKETCH) -o $(BUILD_DIR)$(MAIN_SKETCH).o
}

func saveCodeAndCompile(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	/* this method will get an array of values
	r.ParseForm()
	// save code to file
	fmt.Println(r.Form["code"])
	*/

	var ret *RespStatus
	// make sure Arduino is connected
	port, found := findArduinoPort()
	if found {
		arduinoPort = port
		code := r.FormValue("code")
		fmt.Println("Save code to file")
		filename := saveCodeToFile(code)
		fmt.Println("Start Compile and Upload")
		if compileCode(filename) {
			ret = &RespStatus{200, "OK", "Upload Success"}
		} else {
			ret = &RespStatus{403, "ERROR", "Compile or Upload Error"}
		}
	} else {
		log.Println("Arduino未连接，放弃上传")
		ret = &RespStatus{404, "FAIL", "Arduino Not Connected"}
	}

	resp, _ := json.Marshal(ret)
	fmt.Fprintf(w, string(resp))
}

func addHeaders(w http.ResponseWriter) {
	w.Header().Add("Access-Control-Allow-Origin", "*")
}

var arduinoIsConnected bool = false

// @todo  前台请求当前状态。因为很快，暂时不考虑。
func checkCompileStatus(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	/*
		var ret *RespStatus

			select {
				case
			}
	*/
}

func checkSerialData(serialOp chan string) {
	// 现在Mac不支持，提示 The requested operation is not supported by this system or device
	// 先不管了。
	/*
		type Options struct {
		    Mode        int // read, write; default is read
		    BitRate     int // number of bits per second (baudrate)
		    DataBits    int // number of data bits (5, 6, 7, 8)
		    StopBits    int // number of stop bits (1, 2)
		    Parity      int // none, odd, even, mark, space
		    FlowControl int // none, xonxoff, rtscts, dtrdsr

		    RTS int
		    CTS int
		    DTR int
		    DSR int
		}
	*/
	/*
			options := serial.RawOptions
		    options.BitRate = 9600
			options.Mode = serial.MODE_READ
			options.Parity = serial.PARITY_NONE
			options.FlowControl = serial.FLOWCONTROL_NONE
			//time.Sleep(3 * time.Second)
			//port, err := arduinoPort.Info.Open()
			//port, err := arduinoPort.Info.OpenPort(&options)
			port, err := options.OpenAt(arduinoPort.Info)
	*/
	/*
		options := serial.RawOptions
		options.BitRate = 9600

		port, err := options.Open(arduinoPort.Name)

		if err != nil {
			log.Println("Open Port Failed: ")
			log.Println(err)
			return
		}

		defer port.Close()
	*/
	/* 上边那个go-serial模块有各种问题，比如不支持MacOS下打开端口，无数据时会阻塞等，所以只用它列出端口号*/
	if !arduinoIsConnected {
		waitPort() // 等待连接
	}

	c := &tserial.Config{Name: arduinoPort.Name, Baud: 115200, ReadTimeout: time.Second * 2}
	port, err := tserial.OpenPort(c)
	if err != nil {
		log.Println("Open Port Faild", err)
	}
	arduinoIsConnected = true

	defer port.Close()
	errorCount := 0
	for {
		select {
		case str := <-serialOp:
			if strings.EqualFold(str, "close") {
				return
			}
			if strings.EqualFold(str, "wait") {
				time.Sleep(10 * time.Millisecond)
			}
		default:
			buf := make([]byte, 100)
			if count, err := port.Read(buf); err != nil && err != io.EOF {
				log.Println("Read error: ")
				log.Println(err)
				errorCount++
				if errorCount > 10 {
					//读取错误次数过多，考虑已经断开连接，重新等待连接
					arduinoIsConnected = false
					go checkSerialData(serialOpChan)
					return
				}
			} else {
				//log.Printf("Read %d bytes: %s \n", count, string(buf))
				if count > 0 {
					serialData = append(serialData, buf[:count])
				}
			}
			fmt.Println(serialData)
		}
	}
}

func checkReady(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {

	var ret *RespStatus
	if arduinoIsConnected {
		if len(serialData) > 0 {
			log.Printf("读取到%d字节", len(serialData))
			serialBuf := make([]byte, 0, 100)
			serialOpChan <- "wait" //稍等一下再读，防止在下面循环的过程中，数据丢失
			for _, buf := range serialData {
				serialBuf = append(serialBuf, buf...)
			}
			log.Println("SerialDataSent: " + string(serialBuf))
			serialData = make([][]byte, 0, 10)
			ret = &RespStatus{206, "DATA", string(serialBuf)}
		} else {
			ret = &RespStatus{200, "OK", "Arduino Found"}
		}
	} else {

		ret = &RespStatus{404, "FAIL", "Arduino Not Found"}
	}
	//	case data := <-serialDataChan:
	//		log.Println(string(data))

	resp, _ := json.Marshal(ret)
	fmt.Fprintf(w, string(resp))
}

func waitPort() {
	log.Println("等待连接Arduino")
	for {
		port, found := findArduinoPort()
		if found {
			// 已经找到Arduino端口
			fmt.Println("Arduino已连接", port)
			arduinoPort = port
			break
		} else {
			time.Sleep(3 * time.Second)
		}
	}
}

func crossdomain(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprintf(w, `<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <allow-access-from domain="*" to-port="*"/>
    <allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>`)
}
func optionResponse(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	w.Header().Add("Access-Control-Allow-Headers", "X-Requested-With, Content-type, X-CSRF-Token")
}

func pollResponse(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprintf(w, "OK")
}

type LogRouter struct {
	LogLevel  string
	LogMethod string
	Router    *httprouter.Router
}

func (router LogRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	if !strings.EqualFold(router.LogLevel, "none") {
		log.Println(r.URL.Path)
	}
	addHeaders(w)
	router.Router.ServeHTTP(w, r)
}

func downloadToTempFile(uri string) string {
	tr := &http.Transport{
		TLSClientConfig:    &tls.Config{InsecureSkipVerify: true},
		DisableCompression: true,
	}
	client := &http.Client{Transport: tr}
	client.Jar, _ = cookiejar.New(nil)

	resp, err := client.Get(uri) // Get(uri + time.Now().Format("200601")) 添加月份，方便CDN更新。但是直接从源站获取，可以方便通过日志获取打开次数
	//	body, err := ioutil.ReadAll(resp.Body)
	//	fmt.Println(string(body), err)
	if err != nil {
		log.Fatal("Failed to download SSL cert")
	}

	tmpdir, err := ioutil.TempDir("", "codepku-cert")
	if err != nil {
		log.Fatal("Failed to create temp folder: ", err)
	}
	// defer os.RemoveAll(tmpdir)这里不能删除，后边还要编译上传代码
	filename := tmpdir + string(os.PathSeparator) + path.Base(uri)
	fout, err := os.Create(filename)
	log.Println("Create folder and file: " + fout.Name())
	_, err = io.Copy(fout, resp.Body)
	if err != nil {
		log.Fatal("Failed to write SSL cert to file")
	}
	defer fout.Close()

	return filename
}

var arduinoPort *PortInfo
var serialData [][]byte
var serialOpChan chan string

func main() {

	//下载local.codepku.com的证书
	certfile := downloadToTempFile(crtFile)
	keyfile := downloadToTempFile(keyFile)
	infofile := downloadToTempFile(infoFile)

	info, _ := ioutil.ReadFile(infofile)
	log.Printf(string(info))

	//检测串口
	serialData = make([][]byte, 0, 10)
	serialOpChan = make(chan string)

	go checkSerialData(serialOpChan)

	//开始监听 http请求

	router := httprouter.New()

	router.GET("/checkReady", checkReady)
	router.GET("/checkCompileStatus", checkCompileStatus)
	router.GET("/poll", pollResponse)
	router.POST("/saveCodeCompile", saveCodeAndCompile)
	router.GET("/crossdomain.xml", crossdomain)
	router.OPTIONS("/*path", optionResponse)
	//router.GET("/hello/:name", Hello)

	//logrouter := LogRouter{"debug", "default", router}
	logrouter := LogRouter{"none", "default", router}

	// err := http.ListenAndServe("127.0.0.1:8081", logrouter)
	err := http.ListenAndServeTLS("127.0.0.1:"+PORT,
		certfile,
		keyfile,
		logrouter)

	if err != nil {
		log.Println("启动失败，请检查"+PORT+"端口是否已被占用，请退出占用该端口的程序或者重启电脑后重新尝试: ", err)
		time.Sleep(10 * time.Second)
	}
}
